- 文章
- 基于后缀算术表达式的代码解析
- 基于AST的算数表达式解析
- Vscode Java 环境配置
- 纯前端实现图片的模板匹配
- 测试用例管理工具Luckyframe安装
- Vscode远程开发,本地翻墙神器
- 记前端手写方法
- Node 2020年新增功能
- yum-404-error
- react16特性:fiber reconciler解密
- cmd终端设置代理
- 前端面试题收集
- git子模块
- 算法-排序
- linux安装python-pyenv环境
- 开发人员良心工具
- 斐波拉契数列js实现
- 数组ArrayFlatten
- Docker安装部署taiga项目
- 极光推送RN集成
- docker-pm2发布node服务
- git-pull获取指定文件
- git获取第一次commit提交记录
- ReactNative项目选型设计
- Docker-Mysql8.0安装及初始化配置
- DDA算法
- ubuntu搭建shadowsocks服务
- React-Native 接入百度统计SDK
- docker-使用yum安装
- 前端入门篇
- CodePush尝试
- Markdown数学公式
- Mongoose踩坑路
- linux系统nvm指定版本安装
- linux安装nginx
- Vscode-Threejs代码智能提示
- linux常用命令
- 说明
react16特性:fiber reconciler解密
五月 06, 2019前篇
React是一款用于构建用户界面的库,React会跟踪组件内的状态变化并将这些变化反应到页面上,当我们使用setState
方法,React会检测组件的状态及属性是否改变,从而重新渲染组件到界面上,这种机制被称为reconciliation(协调)
。
对此机制,React对其中关键要素给出了高度的概括,涉及到:React元素
、生命周期函数
、render方法
、以及判断组件变化的Diff
算法。经过React组件render方法返回的元素树具有不可变性,通常被称为Virtual DOM
,这东西的叫法很早就出现了,用于帮助一些人理解,但目前看来也给许多人带来了困惑,而在这篇文章里,我将坚持称它为React elements tree
。
除了React elements tree
,React框架里还维护着一棵内部实例(组件,Dom节点等)树用于保持应用的状态。从React16开始,React推出一种新的管理内部实例树的算法,而它就是Fiber
。
在本篇文章里,我会对这些重要的概念、涉及到的算法数据结构做一个深度的介绍。
准备例子
这里有一个简单的应用示例,Counter如下:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return { count: state.count + 1 };
});
}
render() {
return (
<div style={{ margin: "200px auto", textAlign: "center" }}>
<span>{this.state.count}</span>
<button onClick={this.handleClick}>click</button>
</div>
);
}
}
如上代码所示,此Counter由一个button
和span
组成,点击button按钮,span的内容会改变+1。
在整个reconciliation期间,React会存在大量的操作,比如在本例子中:
- 更新组件Counter状态中的count属性
- 获取并比较Counter的子节点以及它们的属性
- 更新span的属性
还有一些其他的操作,比如调用生命周期方法(LifeCycle methods)或者更新元素的引用(Refs),所有这一些操作,在Fiber架构中被称为Work(工作单元)
。不同种类的React元素
对应不同的Work,比如Class组件,React需要创建一个组件的实例,而不会在Functional组件上执行。React里有很多种React元素类型,比如class和fuctional组件、host组件(如dom节点)、portals等等。React元素的类型(type)由函数React.crateElement
的第一参数决定。
在正式介绍这些操作和Fiber算法内容之前,我们必须要熟悉下React内部定义的数据结构。
React Elements与Fiber Node
ReactDom在调用render方法(ReactDom.render(<XXX />, container))
的时候,会通过由new ReactRoot
创建Container
,然后并调用root.render();
// 这里代码是从React源码中截取
function ReactRoot(
container: DOMContainer,
isConcurrent: boolean,
hydrate: boolean,
) {
const root = createContainer(container, isConcurrent, hydrate);
this._internalRoot = root;
}
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
const root = this._internalRoot;
const work = new ReactWork();
...
updateContainer(children, root, null, work._onCommit);
...
};
所以我们看看这些参数的数据结构:
container: DOMContainer
比如是document.querySelector("#root")
而在
createContainer
方法中才是生成我们的FiberRoot及NodeTree:// 这里节选了React源码重要的函数调用流程以辅助理解 export function createContainer( containerInfo, isConcurrent, hydrate ) { return createFiberRoot(containerInfo, isConcurrent, hydrate); } export function createFiberRoot( containerInfo, isConcurrent, hydrate ) { const root = new FiberRootNode(containerInfo, hydrate); const uninitializedFiber = createHostRootFiber(isConcurrent); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; return root; } function FiberRootNode(containerInfo, hydrate) { this.current = null; this.containerInfo = containerInfo; this.pendingChildren = null; this.pingCache = null; this.pendingCommitExpirationTime = NoWork; this.finishedWork = null; this.timeoutHandle = noTimeout; this.context = null; this.pendingContext = null; this.hydrate = hydrate; this.firstBatch = null; this.callbackNode = null; this.callbackExpirationTime = NoWork; this.firstPendingTime = NoWork; this.lastPendingTime = NoWork; this.pingTime = NoWork; } export function createHostRootFiber(isConcurrent) { let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext; return createFiber(HostRoot, null, null, mode); } const createFiber = function( tag, pendingProps, key, mode, ) { return new FiberNode(tag, pendingProps, key, mode); }; function FiberNode( tag, pendingProps, key, mode, ) { // Instance this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; // Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.contextDependencies = null; this.mode = mode; // Effects this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; this.expirationTime = NoWork; this.childExpirationTime = NoWork; this.alternate = null; }
如果你已经查看并理解了上面代码的话,此时你就会发现我们的
container dom
已经被包裹了一层,表现如下图所示为:这里只是FiberRoot的部分数据结构。
children: ReactNodeList
这个是ReactElement比如我们在jsx中写的各种组件等引用
class Counter extends Component { render() { return ( <div> <span>{this.state.count}</span> <button onClick={this.handleClick}>click</button> </div> ) } }
如果不是按照jsx的写法,此时会是这样的
class Counter extends Component { render() { return [ React.createElement( 'div', {}, [ React.createElement( 'span', {}, this.state.count ), React.createElement( 'button', { onClick: this.onClick }, 'click' ), ] ) ] } }
render方法中的React.createElement将生成一个对象(ReactElement),其数据结构就如这样:
{ $$typeof: Symbol(react.element), "type": "div", "key": null, "ref": null, "props": { "children": [{ $$typeof: Symbol(react.element), "type": "span", "key": null, "ref": null, "props": { "children": this.state.count }, }, { $$typeof: Symbol(react.element), "type": "button", "key": null, "ref": null, "props": { "onClick": () => { ... }, "children": "click" }, }] }, }
在这些Element中,React给这些对象添加了
$$typeof
属性,用来标识它们是react元素,然后element上有type、key、props等,这都取决于你传入createElement的参数。
在reconciliation期间,React组件的render所产生的ReactElement会被合并到Fiber树中,并且每一个ReactElement都有一个对应的Fiber节点。对于不同类型的元素,React需要做不同的操作。在我们的例子中,对于class组件Counter,React会调用生命周期方法、render方法;而对于span元素,其被称为host组件(DOM节点),React会执行必要的DOM变更。因此,每一个React元素,根据其类型转为对应类型的Fiber节点,而Fiber节点的类型告诉React要对此类节点做些什么。
当一个React元素首次被转换为Fiber节点的时候,React会使用Element对象作为参数,来调用createFiberFromTypeAndProps。随后的更新,React会复用这些Fiber节点,根据新生成的element数据,仅仅更新那些需要改变的属性。React可能会根据元素的key属性,在Fiber结构中移动或移除节点。
因为React为每个React元素创建一个Fiber节点,并且可以从Fiber的数据结构中return child sibling
字段看出,这些元素将组成的一个Fiber节点树(其实应该说是链式节点更准确)。对于我们的示例应用程序,它看起来像这样:
为什么会使用链式存储Fiber节点,请看另一篇文章
首次渲染之后,React便拥有了一颗Fiber节点树,它是用以呈现当前页面UI的状态。而这颗树在Fiber里也被引用为current
,
const uninitializedFiber = createHostRootFiber(isConcurrent);
root.current = uninitializedFiber;
而当应用状态更新的时候,React会构建一个workInProgress
tree(其实也是一个链表),它用来更新一个未来的UI状态,所有的操作都发生在workInProgress
树上。在React遍历current
树时候,会对fiber节点创建一个克隆的alternate
节点,这些alternate
节点就会组建成workInProgress
树。应用的状态更新发生在alternate节点上,当全部的alternate节点的更新(work)得到处理完成时(还未重新绘制到界面上,只是节点属性的更新了),这个时候workInProgress
树就生成完毕。最后workInProgress
树被绘制到界面上后,这个workInProgress
树就会变成current
树。
React根据遍历WrokInProgress树,在每一个节点中会根据WrokInProgress树current的tag,也就是Fiber的标识符,用来区分比如updateFunctionalComponent
、updateClassComponent
等操作,这些函数调用里会构造下一节点child,比如updateFunctionComponent -> nextChildren = renderWithHooks()
, updateClassComponent -> nextChildren = intance.render()
;
switch (workInProgress.tag) {
case IndeterminateComponent: {
...
}
case LazyComponent: {
...
}
case FunctionComponent: {
...
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case ClassComponent: {
...
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case HostRoot:
...
case HostComponent:
...
case HostText:
...
case SuspenseComponent:
...
case HostPortal:
...
case ForwardRef: {
...
}
case Fragment:
...
case Mode:
...
case Profiler:
...
case ContextProvider:
...
case ContextConsumer:
...
case MemoComponent: {
...
}
case SimpleMemoComponent: {
...
}
case IncompleteClassComponent: {
...
}
case DehydratedSuspenseComponent: {
...
}
case EventComponent: {
...
}
case EventTarget: {
...
}
}
function updateFunctionComponent() {
...
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
}
function updateClassComponent() {
...
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
}
function finishCalssComponent() {
...
nextChildren = instance.render();
}
在这些Update操作过程中,每一个Fiber节点都会查看是否有子节点,并计算出来,就得到了nextChildren
,也就是ReactELement,再通过reconcileChildren
并把它封装包裹到Fiber节点中,然后组成Fiber和workInProgress节点树。
function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
根据之前说到的,workInProgress树遍历完后,这个时候只是FiberNode的alternateNode,并没有呈现到ui上,为此在没有呈现到ui上的阶段,在React里被称为Render Phase
,这一阶段是可以被中断, 而workInProgress树遍历完后,即Render Phase结束后,会有一个标识符workInProgressRootExitStatus = RootCompleted,也就是completeWork()函数执行完后,会将此标志workInProgressRootExitStatus赋值为RootCompleted
,此时workInProgress current也为null了
,则会退出workLoop
, 然后根据workInProgressRootExitStatus
判断Loop结束状态为RootCompleted
,则React进入到Commit Phase
,这一阶段是不能被中断的。涉及到以下函数调用过程:
function renderRoot(
root: FiberRoot,
expirationTime: ExpirationTime,
isSync: boolean,
): SchedulerCallback | null {
do {
try {
if (isSync) {
workLoopSync();
} else {
workLoop();
}
break;
} catch (thrownValue) {
...
}
} while (true);
switch (workInProgressRootExitStatus) {
...
case RootCompleted: {
// The work completed. Ready to commit.
return commitRoot.bind(null, root, expirationTime);
}
}
}
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function workLoop() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
...
next = beginWork(current, unitOfWork, renderExpirationTime);
if (next === null) {
next = completeUnitOfWork(unitOfWork);
}
...
return next;
}
beginWork = (current, unitOfWork, expirationTime) => {
...
return originalBeginWork(current, unitOfWork, expirationTime);
};
function originalBeginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
...
switch (workInProgress.tag) {
...
return updateClassComponent()
}
}
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
...
next = completeWork(current, workInProgress, renderExpirationTime);
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
...
switch (workInProgress.tag) {
...
}
}
可参考以下流程图:
![01.jpg]
https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVEQ7XG5yb290LS0-RmliZXJSb290O1xuRmliZXJSb290W2N1cnJlbnRdLS0-SG9zdEZpYmVySG9zdDtcbkhvc3RGaWJlckhvc3Rbc3RhdGVOb2RlXS0tPnJvb3Q7XG5GaWJlclJvb3RbY29udGFpbmVySW5mb10tLT5jb250YWluZXJEb21cbmNvbnRhaW5lckRvbS0tPkZpYmVyUm9vdFtjb250YWluZXJJbmZvXSIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In19
graph TD;
FiberRoot–>FiberRootNode;
FiberRootNode–>FiberRoot;
FiberRootNode–>FiberRootNode.containerInfo
FiberRootNode.containerInfo–>containerDom
containerDom–>FiberRootNode.containerInfo
FiberRootNode–FiberRootNode.current–>HostFiberHost;
HostFiberHost–HostFiberHost.stateNode–>FiberRoot
![02.jpg]
graph LR;
HostRoot–child–>Counter
Counter–return–>HostRoot
Counter–child–>div
div–return–>Counter
div–child–>span
span–return–>div
span–sibling–>button
button–return–>div